#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>    //uintptr_t
#include <stdarg.h>    //va_start....
#include <unistd.h>    //STDERR_FILENO等
#include <sys/time.h>  //gettimeofday
#include <time.h>      //localtime_r
#include <fcntl.h>     //open
#include <errno.h>     //errno

#include "ngx_macro.h"
#include "ngx_c_conf.h"
#include "ngx_func.h"
#include "ngx_global.h"

//全局量---------------------
//错误等级，和ngx_macro.h里定义的日志等级宏是一一对应关系
static u_char err_levels[][20]  = 
{
    {"stderr"},    //0：控制台错误
    {"emerg"},     //1：紧急
    {"alert"},     //2：警戒
    {"crit"},      //3：严重
    {"error"},     //4：错误
    {"warn"},      //5：警告
    {"notice"},    //6：注意
    {"info"},      //7：信息
    {"debug"}      //8：调试
};
ngx_log_t ngx_log;

//和日志，打印输出有关
// log初始化
void ngx_log_init()
{
    u_char *plogname = NULL;
    size_t nlen;

    // 从配置文件中读取和日志相关的配置信息
    CConfig *p_config = CConfig::GetInstance();
    plogname = (u_char *)p_config->GetString("Log");
    if(plogname == NULL)
    {
        // 没读到，给个缺省的路径文件名
        // "logs/error.log" ,logs目录需要提前建立出来
        plogname = (u_char *) NGX_ERROR_LOG_PATH;
    }
    // 缺省日志等级为6【注意】 ，如果读失败，就给缺省日志等级
    ngx_log.log_level = p_config->GetIntDefault("LogLevel", NGX_LOG_NOTICE);

    // O_DIRECT: 绕过内核缓冲区，write()成功则写磁盘必然成功，但效率可能会比较低；
    ngx_log.fd = open((const char *)plogname, O_WRONLY|O_APPEND|O_CREAT,0644);  
    if (ngx_log.fd == -1)
    {// 如果有错误，则直接定位到 标准错误上去 
        ngx_log_stderr(errno, "[alert] could not open error log file: open() \"%s\" failed", plogname);
        ngx_log.fd = STDERR_FILENO; //直接定位到标准错误去了        
    } 
    return;
}

// 往屏幕上打印错误信息
void ngx_log_stderr(int err, const char *fmt, ...)
{
    va_list args;
    u_char errstr[NGX_MAX_ERROR_STR + 1] = { 0 };
    u_char *p, *pLast;

    // pLast 指向最后一个有效位置的后面也就是非有效位，主要是做一个标记
    pLast = errstr + NGX_MAX_ERROR_STR;

    // p指针指向errstr的nginx: 的后面
    p = ngx_cpymem(errstr, "nginx: ", 7);
    // 使args指向起始的参数
    va_start(args, fmt);
    // 组合出这个字符串放在errstr里面
    p = ngx_vslprintf(p, pLast, fmt, args);
    // 释放args
    va_end(args);

    if (err)
    {// 如果错误代码不是0，表示有错误发生
        p = ngx_log_errno(p, pLast, err);
    }

    // 若位置不够，那换行也要硬插入到末尾，哪怕覆盖到其他内容    
    if (p >= (pLast - 1))
    {
        // 把尾部空格留出来
        // pLast-1，才是最后 一个而有效的内存，而这个位置要保存\0，
        // 所以我认为再减1，这个位置，才适合保存\n
        p = (pLast - 1) - 1;
    }
    *p++ = '\n'; //增加个换行符    
    
    // 往标准错误输出信息
    write(STDERR_FILENO, errstr, p - errstr);

    // 如果作为守护进程启动的话，是输出不了了，所以在日志上也要写入
    if (ngx_log.fd > STDERR_FILENO)
    {// 表示打开了日志文件
        ngx_log_error_core(NGX_LOG_EMERG, err, (const char *)errstr);
    }
    
    return;
}

//----------------------------------------------------------------------------------------------------------------------
//往日志文件中写日志，代码中有自动加换行符，所以调用时字符串不用刻意加\n；
//    日过定向为标准错误，则直接往屏幕上写日志【比如日志文件打不开，则会直接定位到标准错误，此时日志就打印到屏幕上，参考ngx_log_init()】
//level:一个等级数字，我们把日志分成一些等级，以方便管理、显示、过滤等等，如果这个等级数字比配置文件中的等级数字"LogLevel"大，那么该条信息不被写到日志文件中
//err：是个错误代码，如果不是0，就应该转换成显示对应的错误信息,一起写到日志文件中，
//ngx_log_error_core(5,8,"这个XXX工作的有问题,显示的结果是=%s","YYYY");
void ngx_log_error_core(int level,  int err, const char *fmt, ...)
{
    u_char  *last;
    u_char  errstr[NGX_MAX_ERROR_STR+1] = { 0 };
    last = errstr + NGX_MAX_ERROR_STR;   
    
    struct timeval   tv = { 0 };
    struct tm        tm = { 0 };
    time_t           sec;   // 秒
    u_char           *p;    // 指向当前要拷贝数据到其中的内存位置
    va_list          args;

    // 获取当前时间戳【第二个参数是时区，一般不关心】        
    gettimeofday(&tv, NULL);

    sec = tv.tv_sec;        // 秒
    localtime_r(&sec, &tm); // 把参数1的time_t转换为本地时间，保存到参数2中去，带_r的是线程安全的版本，尽量使用
    ++tm.tm_mon;            // 月份要调整下正常
    tm.tm_year += 1900;     // 年份要调整下才正常
    u_char strcurrtime[40] = { 0 };  // 先组合出一个当前时间字符串，格式形如：2019/01/08 19:57:11
    ngx_slprintf(strcurrtime,  
                (u_char *)-1,                   // 若用一个u_char *接一个 (u_char *)-1,则 得到的结果是 0xffffffff....，这个值足够大
                "%4d/%02d/%02d %02d:%02d:%02d", // 格式是 年/月/日 时:分:秒
                tm.tm_year, tm.tm_mon,
                tm.tm_mday, tm.tm_hour,
                tm.tm_min, tm.tm_sec);
    // 日期增加进来，得到形如：     2019/01/08 20:26:07
    p = ngx_cpymem(errstr,strcurrtime,strlen((const char *)strcurrtime));
    // 日志级别增加进来，得到形如：  2019/01/08 20:26:07 [crit]
    p = ngx_slprintf(p, last, " [%s] ", err_levels[level]); 
    // 支持%P格式，进程id增加进来，得到形如：   2019/01/08 20:50:15 [crit] 2037:
    p = ngx_slprintf(p, last, "%P: ", ngx_pid);
    va_start(args, fmt);                     //使args指向起始的参数
    p = ngx_vslprintf(p, last, fmt, args);   //把fmt和args参数弄进去，组合出来这个字符串
    va_end(args);                            //释放args 
    if (err)
    {// 如果错误代码不是0，表示有错误发生
        // 错误代码和错误信息也要显示出来
        p = ngx_log_errno(p, last, err);
    }
    // 若位置不够，那换行也要硬插入到末尾，哪怕覆盖到其他内容
    if (p >= (last - 1))
    {
        // last-1，才是最后 一个而有效的内存，而这个位置要保存\0，所以我认为再减1，这个位置，才适合保存\n
        p = (last - 1) - 1; // 把尾部空格留出来                             
    }
    *p++ = '\n'; //增加个换行符       

    //这么写代码是图方便：随时可以把流程弄到while后边去；大家可以借鉴一下这种写法
    ssize_t   n;
    while(true)
    {        
        if (level > ngx_log.log_level) 
        {
            // 要打印的这个日志的等级太落后（等级数字太大，比配置文件中的数字大)
            // 这种日志就不打印了
            break;
        }
        // 磁盘是否满了的判断，先算了吧，还是由管理员保证这个事情吧； 

        // 写日志文件        
        n = write(ngx_log.fd, errstr, p - errstr);  // 文件写入成功后，如果中途
        if (n == -1) 
        {
            // 写失败有问题
            if(errno == ENOSPC) // 写失败，且原因是磁盘没空间了
            {
                // 磁盘没空间了
                // 没空间还写个毛线啊
                // 先do nothing吧；
            }
            else
            {
                // 这是有其他错误，那么我考虑把这个错误显示到标准错误设备吧；
                if(ngx_log.fd != STDERR_FILENO) //当前是定位到文件的，则条件成立
                {
                    n = write(STDERR_FILENO, errstr, p - errstr);
                }
            }
        }
        break;
    } // end while    
    return;
}

//----------------------------------------------------------------------------------------------------------------------
//描述：给一段内存，一个错误编号，我要组合出一个字符串，形如：   (错误编号: 错误原因)，放到给的这段内存中去
//buf：是个内存，要往这里保存数据
//last：放的数据不要超过这里
//err：错误编号，我们是要取得这个错误编号对应的错误字符串，保存到buffer中
u_char *ngx_log_errno(u_char *buf, u_char *last, int err)
{
    char *perrorinfo = strerror(err); //根据资料不会返回NULL;
    size_t len = strlen(perrorinfo);

    //然后还要插入一些字符串： (%d:)  
    char leftstr[10] = {0}; 
    sprintf(leftstr," (%d: ",err);
    size_t leftlen = strlen(leftstr);

    char rightstr[] = ") "; 
    size_t rightlen = strlen(rightstr);
    
    size_t extralen = leftlen + rightlen; //左右的额外宽度
    if ((buf + len + extralen) < last)
    {// 保证整个我装得下，我就装，否则我全部抛弃
        buf = ngx_cpymem(buf, leftstr, leftlen);
        buf = ngx_cpymem(buf, perrorinfo, len);
        buf = ngx_cpymem(buf, rightstr, rightlen);
    }
    return buf;
}
